/**
 * Copyright 2013 Google, Inc.
 * Copyright 2015 Vivliostyle Inc.
 *
 * Vivliostyle.js is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Vivliostyle.js is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Vivliostyle.js.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @fileoverview Apply CSS cascade to a document incrementally and cache the result.
 */
goog.provide('adapt.cssstyler');

goog.require('goog.asserts');
goog.require('adapt.vtree');
goog.require('adapt.csscasc');
goog.require('adapt.xmldoc');
goog.require('adapt.cssprop');

/**
 * @constructor
 */
adapt.cssstyler.SlipRange = function(endStuckFixed, endFixed, endSlipped) {
    /** @type {number} */ this.endStuckFixed = endStuckFixed;
    /** @type {number} */ this.endFixed = endFixed;
    /** @type {number} */ this.endSlipped = endSlipped;
};

/**
 * Maps all ints in a range ("fixed") to ints with slippage ("slipped")
 * @constructor
 */
adapt.cssstyler.SlipMap = function() {
    /** @const */ this.map = /** @type {Array.<adapt.cssstyler.SlipRange>} */ ([]);
};

/**
 * @return {number}
 */
adapt.cssstyler.SlipMap.prototype.getMaxFixed = function() {
    if (this.map.length == 0)
        return 0;
    var range = this.map[this.map.length - 1];
    return range.endFixed;
};

/**
 * @return {number}
 */
adapt.cssstyler.SlipMap.prototype.getMaxSlipped = function() {
    if (this.map.length == 0)
        return 0;
    var range = this.map[this.map.length - 1];
    return range.endSlipped;
};

/**
 * @param {number} endFixed
 * @return {void}
 */
adapt.cssstyler.SlipMap.prototype.addStuckRange = function(endFixed) {
    if (this.map.length == 0) {
        this.map.push(new adapt.cssstyler.SlipRange(endFixed, endFixed, endFixed));
    } else {
        var range = this.map[this.map.length - 1];
        var endSlipped = range.endSlipped + endFixed - range.endFixed;
        if (range.endFixed == range.endStuckFixed) {
            range.endFixed = endFixed;
            range.endStuckFixed = endFixed;
            range.endSlipped = endSlipped;
        } else {
            this.map.push(new adapt.cssstyler.SlipRange(endFixed, endFixed, endSlipped));
        }
    }
};

/**
 * @param {number} endFixed
 * @return {void}
 */
adapt.cssstyler.SlipMap.prototype.addSlippedRange = function(endFixed) {
    if (this.map.length == 0) {
        this.map.push(new adapt.cssstyler.SlipRange(endFixed, 0, 0));
    } else {
        this.map[this.map.length - 1].endFixed = endFixed;
    }
};

/**
 * @param {number} fixed
 * @return {number}
 */
adapt.cssstyler.SlipMap.prototype.slippedByFixed = function(fixed) {
    var self = this;
    var index = adapt.base.binarySearch(this.map.length, function(index) {
        return fixed <= self.map[index].endFixed;
    });
    var range = this.map[index];
    return range.endSlipped - Math.max(0, range.endStuckFixed - fixed);
};

/**
 * Smallest fixed for a given slipped.
 * @param {number} slipped
 * @return {number}
 */
adapt.cssstyler.SlipMap.prototype.fixedBySlipped = function(slipped) {
    var self = this;
    var index = adapt.base.binarySearch(this.map.length, function(index) {
        return slipped <= self.map[index].endSlipped;
    });
    var range = this.map[index];
    return range.endStuckFixed - (range.endSlipped - slipped);
};

/**
 * @interface
 */
adapt.cssstyler.FlowListener = function() {};

/**
 * @param {adapt.vtree.FlowChunk} flowChunk
 * @param {adapt.vtree.Flow} flow
 * @return void
 */
adapt.cssstyler.FlowListener.prototype.encounteredFlowChunk = function(flowChunk, flow) {};

/**
 * @interface
 */
adapt.cssstyler.AbstractStyler = function() {};

/**
 * @param {Element} element
 * @param {boolean} deep
 * @return {adapt.csscasc.ElementStyle}
 */
adapt.cssstyler.AbstractStyler.prototype.getStyle = function(element, deep) {};

/**
 * @param {Element} element
 * @param {!Object.<string,adapt.css.Val>} styles
 */
adapt.cssstyler.AbstractStyler.prototype.processContent = function(element, styles) {};

/**
 * Represent a box generated by a (pseudo)element. When constructed, a box corresponding to `::before` pseudoelement is also constructed and stored in `beforeBox` property, whereas one corresponding `::after` pseudoelement is not constructed and `afterBox` property is `null`. `afterBox` is constructed by `buildAfterPseudoElementBox` method.
 * @param {adapt.expr.Context} context
 * @param {!adapt.csscasc.ElementStyle} style Cascaded style values for the box.
 * @param {number} offset The start offset of the box. It coincides with the start offset of the element if the box is generated by the element or the `::before` pseudoelement. When the box corresponds to the `::after` pseudoelement, the offset is just after the content before the `::after` pseudoelement.
 * @param {boolean} isRoot True if the box is generated by the root element (not pseudoelement).
 * @param {!adapt.vtree.FlowChunk} flowChunk FlowChunk to which the box belongs to.
 * @param {boolean} atBlockStart True if the box is right after the block start edge.
 * @param {boolean} atFlowStart True if the box is right after the flow start.
 * @param {boolean} isParentBoxDisplayed True if the parent box has a displayed box.
 * @constructor
 */
adapt.cssstyler.Box = function(context, style, offset, isRoot, flowChunk, atBlockStart, atFlowStart,
                               isParentBoxDisplayed) {
    /** @const */ this.context = context;
    /** @const */ this.style = style;
    /** @const */ this.offset = offset;
    /** @const */ this.isRoot = isRoot;
    /** @const */ this.flowChunk = flowChunk;
    /** @const */ this.flowName = flowChunk.flowName;
    /** @const */ this.atBlockStart = atBlockStart;
    /** @const */ this.atFlowStart = atFlowStart;
    /** @const */ this.isParentBoxDisplayed = isParentBoxDisplayed;

    /** @type {?boolean} */ this.isBlockValue = null;
    /** @type {?boolean} */ this.hasBoxValue = null;
    /** @const */ this.styleValues = /** @type {Object<string, adapt.css.Val>} */ ({});

    /** @type {adapt.cssstyler.Box} */ this.beforeBox = null;
    /** @type {adapt.cssstyler.Box} */ this.afterBox = null;
    /** @type {?string} */ this.breakBefore = null;
    if (this.hasBox()) {
        var pseudoMap = style["_pseudos"];
        if (pseudoMap) {
            if (pseudoMap["before"]) {
                var beforeBox = new adapt.cssstyler.Box(context, pseudoMap["before"], offset, false, flowChunk,
                    this.isBlock(), atFlowStart, true);
                var beforeContent = beforeBox.styleValue("content");
                if (adapt.vtree.nonTrivialContent(beforeContent)) {
                    this.beforeBox = beforeBox;
                    this.breakBefore = beforeBox.breakBefore;
                }
            }
        }
    }
    this.breakBefore = vivliostyle.break.resolveEffectiveBreakValue(this.getBreakValue("before"), this.breakBefore);
    if (this.atFlowStart && vivliostyle.break.isForcedBreakValue(this.breakBefore)) {
        flowChunk.breakBefore = vivliostyle.break.resolveEffectiveBreakValue(flowChunk.breakBefore, this.breakBefore);
    }
};

/**
 * Build a box corresponding to `::after` pseudoelement and stores it in `afterBox` property.
 * @param {number} offset The start offset of the `::after` pseudoelement box, which is just after the content before the `::after` pseudoelement.
 * @param {boolean} atBlockStart True if the box is right after the block start edge.
 * @param {boolean} atFlowStart True if the box is right after the flow start.
 */
adapt.cssstyler.Box.prototype.buildAfterPseudoElementBox = function(offset, atBlockStart, atFlowStart) {
    if (this.hasBox()) {
        var pseudoMap = this.style["_pseudos"];
        if (pseudoMap) {
            if (pseudoMap["after"]) {
                var afterBox = new adapt.cssstyler.Box(this.context, pseudoMap["after"], offset, false,
                    this.flowChunk, atBlockStart, atFlowStart, true);
                var afterContent = afterBox.styleValue("content");
                if (adapt.vtree.nonTrivialContent(afterContent)) {
                    this.afterBox = afterBox;
                }
            }
        }
    }
};

/**
 * @param {string} name
 * @param {adapt.css.Val=} defaultValue
 * @returns {?adapt.css.Val}
 */
adapt.cssstyler.Box.prototype.styleValue = function(name, defaultValue) {
    if (!(name in this.styleValues)) {
        var cv = this.style[name];
        this.styleValues[name] = cv ? cv.evaluate(this.context, name) : (defaultValue || null);
    }
    return this.styleValues[name];
};

/**
 * @returns {adapt.css.Val}
 */
adapt.cssstyler.Box.prototype.displayValue = function() {
    return this.styleValue("display", adapt.css.ident.inline);
};

/**
 * @returns {boolean}
 */
adapt.cssstyler.Box.prototype.isBlock = function() {
    if (this.isBlockValue === null) {
        var display = /** @type {!adapt.css.Ident} */ (this.displayValue());
        var position = /** @type {adapt.css.Ident} */ (this.styleValue("position"));
        var float = /** @type {adapt.css.Ident} */ (this.styleValue("float"));
        this.isBlockValue = vivliostyle.display.isBlock(display, position, float, this.isRoot);
    }
    return this.isBlockValue;
};

/**
 * @returns {boolean}
 */
adapt.cssstyler.Box.prototype.hasBox = function() {
    if (this.hasBoxValue === null) {
        this.hasBoxValue = this.isParentBoxDisplayed && this.displayValue() !== adapt.css.ident.none;
    }
    return this.hasBoxValue;
};

/**
 * @param {string} edge
 * @returns {?string}
 */
adapt.cssstyler.Box.prototype.getBreakValue = function(edge) {
    var breakValue = null;
    if (this.isBlock()) {
        var val = this.styleValue("break-" + edge);
        if (val)
            breakValue = val.toString();
    }
    return breakValue;
};

/**
 * Manages boxes generated by elements as a stack.
 * @param {adapt.expr.Context} context
 * @constructor
 */
adapt.cssstyler.BoxStack = function(context) {
    /** @const */ this.context = context;
    /** @const */ this.stack = /** @type {Array<!adapt.cssstyler.Box>} */ ([]);
    /** @type {boolean} */ this.atBlockStart = true; // indicates if the next pushed box will be at the block start
    /** @type {boolean} */ this.atFlowStart = true; // indicates if the next pushed box will be at the flow start
    /** @const */ this.atStartStack =
        /** @type {Array<!{atBlockStart: boolean, atFlowStart: boolean}>} */ ([]); // pushed when a new flow starts
};

/**
 * Returns if the stack is empty.
 * @returns {boolean}
 */
adapt.cssstyler.BoxStack.prototype.empty = function() {
    return this.stack.length === 0;
};

/**
 * Returns the last box in the stack.
 * @returns {adapt.cssstyler.Box|undefined}
 */
adapt.cssstyler.BoxStack.prototype.lastBox = function() {
    return this.stack[this.stack.length - 1];
};

/**
 * Returns the flow name of the last box in the stack.
 * @returns {?string}
 */
adapt.cssstyler.BoxStack.prototype.lastFlowName = function() {
    var lastBox = this.lastBox();
    return lastBox ? lastBox.flowChunk.flowName : null;
};

/**
 * Returns if the last box in the stack is displayed.
 * @returns {boolean}
 */
adapt.cssstyler.BoxStack.prototype.isCurrentBoxDisplayed = function() {
    return this.stack.every(function(box) {
        return box.displayValue() !== adapt.css.ident.none;
    });
};

/**
 * Create a new box and push it to the stack.
 * @param {!adapt.csscasc.ElementStyle} style Cascaded style values for the box.
 * @param {number} offset The start offset of the box.
 * @param {boolean} isRoot True if the box is generated by the root element
 * @param {!adapt.vtree.FlowChunk=} newFlowChunk Specify if the element is a flow element (i.e. the element is specified a new `flow-into` value)
 * @returns {!adapt.cssstyler.Box}
 */
adapt.cssstyler.BoxStack.prototype.push = function(style, offset, isRoot, newFlowChunk) {
    var lastBox = this.lastBox();
    if (newFlowChunk && lastBox && newFlowChunk.flowName !== lastBox.flowName) {
        this.atStartStack.push({
            atBlockStart: this.atBlockStart,
            atFlowStart: this.atFlowStart
        });
    }
    var flowChunk = newFlowChunk || lastBox.flowChunk;
    var isAtFlowStart = this.atFlowStart || !!newFlowChunk;
    var isParentBoxDisplayed = this.isCurrentBoxDisplayed();
    var box = new adapt.cssstyler.Box(this.context, style, offset, isRoot, flowChunk,
        isAtFlowStart || this.atBlockStart, isAtFlowStart, isParentBoxDisplayed);
    this.stack.push(box);

    this.atBlockStart = box.hasBox() ? (!box.beforeBox && box.isBlock()) : this.atBlockStart;
    this.atFlowStart = box.hasBox() ? (!box.beforeBox && isAtFlowStart) : this.atFlowStart;

    return box;
};

/**
 * @param {Node} node
 */
adapt.cssstyler.BoxStack.prototype.encounteredTextNode = function(node) {
    var box = this.lastBox();
    if ((this.atBlockStart || this.atFlowStart) && box.hasBox()) {
        var whitespaceValue = box.styleValue("white-space", adapt.css.ident.normal).toString();
        var whitespace = adapt.vtree.whitespaceFromPropertyValue(whitespaceValue);
        goog.asserts.assert(whitespace !== null);
        if (!adapt.vtree.canIgnore(node, whitespace)) {
            this.atBlockStart = false;
            this.atFlowStart = false;
        }
    }
};

/**
 * Pop the last box.
 * @param {number} offset
 * @returns {!adapt.cssstyler.Box}
 */
adapt.cssstyler.BoxStack.prototype.pop = function(offset) {
    var box = this.stack.pop();
    box.buildAfterPseudoElementBox(offset, this.atBlockStart, this.atFlowStart);
    if (this.atFlowStart && box.afterBox) {
        var breakBefore = box.afterBox.getBreakValue("before");
        box.flowChunk.breakBefore =
            vivliostyle.break.resolveEffectiveBreakValue(box.flowChunk.breakBefore, breakBefore);
    }

    var parent = this.lastBox();
    if (parent) {
        if (parent.flowName === box.flowName) {
            if (box.hasBox()) {
                this.atBlockStart = this.atFlowStart = false;
            }
        } else {
            var atStart = this.atStartStack.pop();
            this.atBlockStart = atStart.atBlockStart;
            this.atFlowStart = atStart.atFlowStart;
        }
    }
    return box;
};

/**
 * Find the start offset of the nearest block start edge to which the `break-before` value of the box should be propagated. This method can be called when after pushing the box into the stack or after popping the box out of the stack.
 * @param {!adapt.cssstyler.Box} box
 * @returns {number}
 */
adapt.cssstyler.BoxStack.prototype.nearestBlockStartOffset = function(box) {
    if (!box.atBlockStart) {
        return box.offset;
    }
    var i = this.stack.length - 1;
    var parent = this.stack[i];
    // When called just after the box is popped out, the last box in the stack is different from the box and it is the parent of the box. When called after the box is pushed, the last box in the stack is identical to the box and the parent of the box is a box right before the specified box.
    if (parent === box) {
        i--;
        parent = this.stack[i];
    }
    while (i >= 0) {
        if (parent.flowName !== box.flowName) {
            return box.offset;
        }
        if (!parent.atBlockStart) {
            return parent.offset;
        }
        if (parent.isRoot) {
            return parent.offset;
        }
        box = parent;
        parent = this.stack[--i];
    }
    throw new Error("No block start offset found!");
};

/**
 * @param {adapt.xmldoc.XMLDocHolder} xmldoc
 * @param {adapt.csscasc.Cascade} cascade
 * @param {adapt.expr.LexicalScope} scope
 * @param {adapt.expr.Context} context
 * @param {Object.<string,boolean>} primaryFlows
 * @param {adapt.cssvalid.ValidatorSet} validatorSet
 * @param {!adapt.csscasc.CounterListener} counterListener
 * @param {!adapt.csscasc.CounterResolver} counterResolver
 * @constructor
 * @implements {adapt.cssstyler.AbstractStyler}
 */
adapt.cssstyler.Styler = function(xmldoc, cascade, scope, context, primaryFlows, validatorSet, counterListener, counterResolver) {
    /** @const */ this.xmldoc = xmldoc;
    /** @const */ this.root = xmldoc.root;
    /** @const */ this.cascadeHolder = cascade;
    /** @const */ this.scope = scope;
    /** @const */ this.context = context;
    /** @const */ this.validatorSet = validatorSet;
    /** @type {Node} */ this.last = this.root;
    /** @const */ this.rootStyle = /** @type {!adapt.csscasc.ElementStyle} */ ({});
    /** @type {Object.<string,adapt.csscasc.ElementStyle>} */ this.styleMap = {};
    /** @const */ this.flows = /** @type {Object<string, !adapt.vtree.Flow>} */ ({});
    /** @const */ this.flowChunks = /** @type {Array.<adapt.vtree.FlowChunk>} */ ([]);
    /** @type {adapt.cssstyler.FlowListener} */ this.flowListener = null;
    /** @type {?string} */ this.flowToReach = null;
    /** @type {?string} */ this.idToReach = null;
    /** @const */ this.cascade = cascade.createInstance(context, counterListener, counterResolver, xmldoc.lang);
    /** @const */ this.offsetMap = new adapt.cssstyler.SlipMap();
    /** @type {boolean} */ this.primary = true;
    /** @const */ this.primaryStack = /** @type {Array.<boolean>} */ ([]);
    /** @const */ this.primaryFlows = primaryFlows;
    /** @type {boolean} */ this.rootBackgroundAssigned = false;
    /** @type {boolean} */ this.rootLayoutAssigned = false;
    var rootOffset = xmldoc.getElementOffset(this.root);
    /** @type {number} */ this.lastOffset = rootOffset;
    /** @const */ this.breakBeforeValues = /** @type {!Object<number, ?string>} */ ({});
    /** @const */ this.boxStack = new adapt.cssstyler.BoxStack(context);

    this.offsetMap.addStuckRange(rootOffset);
    var style = this.getAttrStyle(this.root);
    this.cascade.pushElement(this.root, style, rootOffset);
    this.postprocessTopStyle(style, false);
    /** @type {boolean} */ this.bodyReached = true;
    switch (this.root.namespaceURI) {
        case adapt.base.NS.XHTML:
        case adapt.base.NS.FB2:
            this.bodyReached = false;
            break;
    }
    this.primaryStack.push(true);
    this.styleMap = {};
    this.styleMap["e" + rootOffset] = style;
    this.lastOffset++;
    this.replayFlowElementsFromOffset(-1);
};

/**
 * @param {adapt.csscasc.ElementStyle} style
 * @param {adapt.cssvalid.ValueMap} map
 * @param {string} name
 * @return {boolean}
 **/
adapt.cssstyler.Styler.prototype.hasProp = function(style, map, name) {
    var cascVal = style[name];
    return cascVal && cascVal.evaluate(this.context) !== map[name];
};

/**
 * @param {adapt.csscasc.ElementStyle} srcStyle
 * @param {adapt.cssvalid.ValueMap} map
 * @return {void}
 **/
adapt.cssstyler.Styler.prototype.transferPropsToRoot = function(srcStyle, map) {
    for (var pname in map) {
        var cascval = srcStyle[pname];
        if (cascval) {
            this.rootStyle[pname] = cascval;
            delete srcStyle[pname];
        } else {
            var val = map[pname];
            if (val) {
                this.rootStyle[pname] = new adapt.csscasc.CascadeValue(val, adapt.cssparse.SPECIFICITY_AUTHOR);
            }
        }
    }
};

/**
 * @const
 */
adapt.cssstyler.columnProps = ["column-count", "column-width"];

/**
 * Transfer properties that should be applied on the container (partition) level
 * to this.rootStyle.
 * @param {adapt.csscasc.ElementStyle} elemStyle (source) element style
 * @param {boolean} isBody
 * @return {void}
 */
adapt.cssstyler.Styler.prototype.postprocessTopStyle = function(elemStyle, isBody) {
    if (!isBody) {
        ["writing-mode", "direction"].forEach(function(propName) {
            if (elemStyle[propName]) {
                // Copy it over, but keep it at the root element as well.
                this.rootStyle[propName] = elemStyle[propName];
            }
        }, this);
    }
    if (!this.rootBackgroundAssigned) {
        var backgroundColor = /** @type {adapt.css.Val} */
            (this.hasProp(elemStyle, this.validatorSet.backgroundProps, "background-color") ?
                elemStyle["background-color"].evaluate(this.context) : null);
        var backgroundImage = /** @type {adapt.css.Val} */
            (this.hasProp(elemStyle, this.validatorSet.backgroundProps, "background-image") ?
                elemStyle["background-image"].evaluate(this.context) : null);
        if ((backgroundColor && backgroundColor !== adapt.css.ident.inherit) ||
            (backgroundImage && backgroundImage !== adapt.css.ident.inherit)) {
            this.transferPropsToRoot(elemStyle, this.validatorSet.backgroundProps);
            this.rootBackgroundAssigned = true;
        }
    }
    if (!this.rootLayoutAssigned) {
        for (var i = 0; i < adapt.cssstyler.columnProps.length; i++) {
            if (this.hasProp(elemStyle, this.validatorSet.layoutProps, adapt.cssstyler.columnProps[i])) {
                this.transferPropsToRoot(elemStyle, this.validatorSet.layoutProps);
                this.rootLayoutAssigned = true;
                break;
            }
        }
    }
    if (!isBody) {
        var fontSize = elemStyle["font-size"];
        if (fontSize) {
            var val = fontSize.evaluate(this.context);
            var px = val.num;
            switch (val.unit) {
                case "em":
                case "rem":
                    px *= this.context.initialFontSize;
                    break;
                case "ex":
                case "rex":
                    px *= this.context.initialFontSize * adapt.expr.defaultUnitSizes["ex"] / adapt.expr.defaultUnitSizes["em"];
                    break;
                case "%":
                    px *= this.context.initialFontSize / 100;
                    break;
                default:
                    var unitSize = adapt.expr.defaultUnitSizes[val.unit];
                    if (unitSize) {
                        px *= unitSize;
                    }
            }
            this.context.rootFontSize = px;
        }
    }
};

/**
 * @return {!adapt.csscasc.ElementStyle}
 */
adapt.cssstyler.Styler.prototype.getTopContainerStyle = function() {
    var offset = 0;
    while (!this.bodyReached) {
        offset += 5000;
        if (this.styleUntil(offset, 0) == Number.POSITIVE_INFINITY)
            break;
    }
    return this.rootStyle;
};
/**
 * @param {Element} elem
 * @return {adapt.csscasc.ElementStyle}
 */
adapt.cssstyler.Styler.prototype.getAttrStyle = function(elem) {
    // skip cases in which elements for XML other than HTML or SVG
    // have 'style' attribute not for CSS declaration
    if (elem.style instanceof CSSStyleDeclaration) {
        var styleAttrValue = elem.getAttribute("style");
        if (styleAttrValue) {
            return adapt.csscasc.parseStyleAttribute(this.scope, this.validatorSet,
                this.xmldoc.url, styleAttrValue);
        }
    }
    return /** @type {adapt.csscasc.ElementStyle} */ ({});
};

/**
 * @return {number} currently reached offset
 */
adapt.cssstyler.Styler.prototype.getReachedOffset = function() {
    return this.lastOffset;
};

/**
 * Replay flow elements that were encountered since the given offset
 * @param {number} offset
 * @return {void}
 */
adapt.cssstyler.Styler.prototype.replayFlowElementsFromOffset = function(offset) {
    if (offset >= this.lastOffset)
        return;
    var context = this.context;
    var rootOffset = this.xmldoc.getElementOffset(this.root);
    if (offset < rootOffset) {
        var rootStyle = this.getStyle(this.root, false);
        goog.asserts.assert(rootStyle);
        var flowName = adapt.csscasc.getProp(rootStyle, "flow-into");
        var flowNameStr = flowName ? flowName.evaluate(context, "flow-into").toString() : "body";
        var newFlowChunk = this.encounteredFlowElement(flowNameStr, rootStyle, this.root, rootOffset);
        if (this.boxStack.empty()) {
            this.boxStack.push(rootStyle, rootOffset, true, newFlowChunk);
        }
    }
    var node = this.xmldoc.getNodeByOffset(offset);
    var nodeOffset = this.xmldoc.getNodeOffset(node, 0, false);
    if (nodeOffset >= this.lastOffset)
        return;
    while (true) {
        if (node.nodeType != 1) {
            nodeOffset += node.textContent.length;
        } else {
            var elem = /** @type {!Element} */ (node);
            if (goog.DEBUG) {
                if (nodeOffset != this.xmldoc.getElementOffset(elem)) {
                    throw new Error("Inconsistent offset");
                }
            }
            var style = this.getStyle(elem, false);
            var flowName = style["flow-into"];
            if (flowName) {
                var flowNameStr = flowName.evaluate(context, "flow-into").toString();
                this.encounteredFlowElement(flowNameStr, style, elem, nodeOffset);
            }
            nodeOffset++;
        }
        if (nodeOffset >= this.lastOffset)
            break;
        var next = node.firstChild;
        if (next == null) {
            while (true) {
                next = node.nextSibling;
                if (next)
                    break;
                node = node.parentNode;
                if (node === this.root) {
                    return;
                }
            }
        }
        node = next;
    }
};

/**
 * @param {adapt.cssstyler.FlowListener} flowListener
 * @return {void}
 */
adapt.cssstyler.Styler.prototype.resetFlowChunkStream = function(flowListener) {
    this.flowListener = flowListener;
    for (var i = 0; i < this.flowChunks.length; i++) {
        this.flowListener.encounteredFlowChunk(this.flowChunks[i], this.flows[this.flowChunks[i].flowName]);
    }
};

/**
 * @param {string} flowName
 */
adapt.cssstyler.Styler.prototype.styleUntilFlowIsReached = function(flowName) {
    this.flowToReach = flowName;
    var offset = 0;
    while (true) {
        if (this.flowToReach == null)
            break;
        offset += 5000;
        if (this.styleUntil(offset, 0) == Number.POSITIVE_INFINITY)
            break;
    }
};

/**
 * @param {string} id
 */
adapt.cssstyler.Styler.prototype.styleUntilIdIsReached = function(id) {
    if (!id) return;
    this.idToReach = id;
    var offset = 0;
    while (true) {
        if (!this.idToReach)
            break;
        offset += 5000;
        if (this.styleUntil(offset, 0) === Number.POSITIVE_INFINITY)
            break;
    }
    this.idToReach = null;
};

/**
 * @private
 * @param {string} flowName
 * @param {adapt.csscasc.ElementStyle} style
 * @param {!Element} elem
 * @param {number} startOffset
 * @return {!adapt.vtree.FlowChunk}
 */
adapt.cssstyler.Styler.prototype.encounteredFlowElement = function(flowName, style, elem, startOffset) {
    var priority = 0;
    var linger = Number.POSITIVE_INFINITY;
    var exclusive = false;
    var repeated = false;
    var last = false;
    var optionsCV = style["flow-options"];
    if (optionsCV) {
        var options = adapt.cssprop.toSet(optionsCV.evaluate(this.context, "flow-options"));
        exclusive = !!options["exclusive"];
        repeated = !!options["static"];
        last = !!options["last"];
    }
    var lingerCV = style["flow-linger"];
    if (lingerCV) {
        linger = adapt.cssprop.toInt(lingerCV.evaluate(this.context, "flow-linger"),
            Number.POSITIVE_INFINITY);
    }
    var priorityCV = style["flow-priority"];
    if (priorityCV) {
        priority = adapt.cssprop.toInt(priorityCV.evaluate(this.context, "flow-priority"), 0);
    }
    var breakBefore = this.breakBeforeValues[startOffset] || null;
    var flow = this.flows[flowName];
    if (!flow) {
        var parentFlowName = this.boxStack.lastFlowName();
        flow = this.flows[flowName] = new adapt.vtree.Flow(flowName, parentFlowName);
    }
    var flowChunk = new adapt.vtree.FlowChunk(flowName, elem,
        startOffset, priority, linger, exclusive, repeated, last, breakBefore);
    this.flowChunks.push(flowChunk);
    if (this.flowToReach == flowName)
        this.flowToReach = null;
    if (this.flowListener)
        this.flowListener.encounteredFlowChunk(flowChunk, flow);
    return flowChunk;
};

/**
 * @param {?string} breakValue
 * @param {number} offset
 * @param {string} flowName
 */
adapt.cssstyler.Styler.prototype.registerForcedBreakOffset = function(breakValue, offset, flowName) {
    if (vivliostyle.break.isForcedBreakValue(breakValue)) {
        var forcedBreakOffsets = this.flows[flowName].forcedBreakOffsets;
        if (forcedBreakOffsets.length === 0 ||
            forcedBreakOffsets[forcedBreakOffsets.length - 1] < offset) {
            forcedBreakOffsets.push(offset);
        }
    }
    var previousValue = this.breakBeforeValues[offset];
    this.breakBeforeValues[offset] = vivliostyle.break.resolveEffectiveBreakValue(previousValue, breakValue);
};

/**
 * @param {number} startOffset current position in the document
 * @param {number} lookup lookup window size for the next page
 * @return {number} lookup offset in the document for the next page
 */
adapt.cssstyler.Styler.prototype.styleUntil = function(startOffset, lookup) {
    var targetSlippedOffset = -1;
    var slippedOffset;
    if (startOffset <= this.lastOffset) {
        slippedOffset = this.offsetMap.slippedByFixed(startOffset);
        targetSlippedOffset = slippedOffset + lookup;
        if (targetSlippedOffset < this.offsetMap.getMaxSlipped()) {
            // got to the desired point
            return this.offsetMap.fixedBySlipped(targetSlippedOffset);
        }
    }
    if (this.last == null) {
        return Number.POSITIVE_INFINITY;
    }
    var context = this.context;
    while (true) {
        var next = this.last.firstChild;
        if (next == null) {
            while (true) {
                if (this.last.nodeType == 1) {
                    this.cascade.popElement(/** @type {Element} */ (this.last));
                    this.primary = this.primaryStack.pop();
                    var box = this.boxStack.pop(this.lastOffset);
                    var breakAfter = null;
                    if (box.afterBox) {
                        var afterPseudoBreakBefore = box.afterBox.getBreakValue("before");
                        this.registerForcedBreakOffset(afterPseudoBreakBefore, box.afterBox.atBlockStart ?
                            this.boxStack.nearestBlockStartOffset(box) : box.afterBox.offset, box.flowName);
                        breakAfter = box.afterBox.getBreakValue("after");
                    }
                    breakAfter = vivliostyle.break.resolveEffectiveBreakValue(breakAfter, box.getBreakValue("after"));
                    this.registerForcedBreakOffset(breakAfter, this.lastOffset, box.flowName);
                }
                next = this.last.nextSibling;
                if (next)
                    break;
                this.last = this.last.parentNode;
                if (this.last === this.root) {
                    this.last = null;
                    if (startOffset < this.lastOffset) {
                        if (targetSlippedOffset < 0) {
                            slippedOffset = this.offsetMap.slippedByFixed(startOffset);
                            targetSlippedOffset = slippedOffset + lookup;
                        }
                        if (targetSlippedOffset <= this.offsetMap.getMaxSlipped()) {
                            // got to the desired point
                            return this.offsetMap.fixedBySlipped(targetSlippedOffset);
                        }
                    }
                    return Number.POSITIVE_INFINITY;
                }
            }
        }
        this.last = next;
        if (this.last.nodeType != 1) {
            this.lastOffset += this.last.textContent.length;
            this.boxStack.encounteredTextNode(this.last);
            if (this.primary)
                this.offsetMap.addStuckRange(this.lastOffset);
            else
                this.offsetMap.addSlippedRange(this.lastOffset);
        } else {
            var elem = /** @type {!Element} */ (this.last);
            var style = this.getAttrStyle(elem);
            this.primaryStack.push(this.primary);
            this.cascade.pushElement(elem, style, this.lastOffset);
            var id = elem.getAttribute("id") || elem.getAttributeNS(adapt.base.NS.XML, "id");
            if (id && id === this.idToReach) {
                this.idToReach = null;
            }
            if (!this.bodyReached && elem.localName == "body" && elem.parentNode == this.root) {
                this.postprocessTopStyle(style, true);
                this.bodyReached = true;
            }
            var box;
            var flowName = style["flow-into"];
            if (flowName) {
                var flowNameStr = flowName.evaluate(context, "flow-into").toString();
                var newFlowChunk = this.encounteredFlowElement(flowNameStr, style, elem, this.lastOffset);
                this.primary = !!this.primaryFlows[flowNameStr];
                box = this.boxStack.push(style, this.lastOffset, elem === this.root, newFlowChunk);
            } else {
                box = this.boxStack.push(style, this.lastOffset, elem === this.root);
            }
            var blockStartOffset = this.boxStack.nearestBlockStartOffset(box);
            this.registerForcedBreakOffset(box.breakBefore, blockStartOffset, box.flowName);
            if (box.beforeBox) {
                var beforePseudoBreakAfter = box.beforeBox.getBreakValue("after");
                this.registerForcedBreakOffset(beforePseudoBreakAfter,
                    box.beforeBox.atBlockStart ? blockStartOffset : box.offset,
                    box.flowName);
            }

            if (this.primary) {
                if (box.displayValue() === adapt.css.ident.none) {
                    this.primary = false;
                }
            }
            if (goog.DEBUG) {
                var offset = this.xmldoc.getElementOffset(/** @type {Element} */ (this.last));
                if (offset != this.lastOffset)
                    throw new Error("Inconsistent offset");
            }
            this.styleMap["e"+this.lastOffset] = style;
            this.lastOffset++;
            if (this.primary)
                this.offsetMap.addStuckRange(this.lastOffset);
            else
                this.offsetMap.addSlippedRange(this.lastOffset);
            if (startOffset < this.lastOffset) {
                if (targetSlippedOffset < 0) {
                    slippedOffset = this.offsetMap.slippedByFixed(startOffset);
                    targetSlippedOffset = slippedOffset + lookup;
                }
                if (targetSlippedOffset <= this.offsetMap.getMaxSlipped()) {
                    // got to the desired point
                    return this.offsetMap.fixedBySlipped(targetSlippedOffset);
                }
            }
        }
    }
};

/**
 * @override
 */
adapt.cssstyler.Styler.prototype.getStyle = function(element, deep) {
    var offset = this.xmldoc.getElementOffset(element);
    var key = "e" + offset;
    if (deep) {
        offset = this.xmldoc.getNodeOffset(element, 0, true);
    }
    if (this.lastOffset <= offset) {
        this.styleUntil(offset, 0);
    }
    return this.styleMap[key];
};

/**
 * @override
 */
adapt.cssstyler.Styler.prototype.processContent = function(element, styles) {};
